package com.core.service.SeatReservation;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.core.MQ.MessageProducer;
import com.core.exception.BusinessException;
import com.core.mapper.ClassroomMapper;
import com.core.mapper.DailyClassroomAvailabilityMapper;
import com.core.mapper.ReservationMapper;
import com.core.pojo.Result.Result;
import com.core.pojo.SeatReservation.Enum.ReservationStatusEnum;
import com.core.pojo.SeatReservation.dto.ReservationDTO;
import com.core.pojo.SeatReservation.entity.Classroom;
import com.core.pojo.SeatReservation.entity.DailyClassroomAvailability;
import com.core.pojo.SeatReservation.entity.Reservation;
import com.core.pojo.User.entity.User;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDate;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.TimeUnit;

import static com.core.constant.LockConstants.*;
import static com.core.constant.RedisConstants.*;

@Service
@Slf4j
@RequiredArgsConstructor
public class ReservationServiceImpl implements ReservationService {

    private final ClassroomMapper classroomMapper;
    private final StringRedisTemplate stringRedisTemplate;
    private final DailyClassroomAvailabilityMapper dailyClassroomAvailabilityMapper;
    private final RedissonClient redissonClient;
    private final ReservationMapper reservationMapper;
    private final MessageProducer messageProducer;


    // 参数验证
    public Result<String> validateRequest(Long classroomId, LocalDate date, Long userId) {
        // 检查教室是否存在
        Classroom classroom = classroomMapper.selectById(classroomId);
        if (classroom == null) {
            return Result.error("教室不存在");
        }
        // 检查日期是否未来（假设date是LocalDate）
        if (date.isBefore(LocalDate.now())) {
            return Result.error("不能预约过去日期");
        }
        // 检查该用户是否在黑名单内
        if(Boolean.TRUE.equals(stringRedisTemplate.opsForSet().isMember(USER_BLACKLIST_KEY, userId.toString()))) {
            return Result.error("该用户恶意占座次数过多，禁止预约");
        }
        // 检查是否已有此人当天的预约记录
        QueryWrapper<Reservation> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("date", date).eq("user_id", userId);
        Reservation queryResult = reservationMapper.selectOne(queryWrapper);
        if (queryResult != null) {
            return Result.error("该用户在当天已经预约过了");
        }
        // TODO 只能使用自己的学号预约

        return Result.success();
    }

    /**
     * 检查并原子扣减Redis中的可用座位
     */
    public Result<String> decrementRedisAvailability(Long classroomId, LocalDate date) {
        String key = DAILY_CLASSROOM_AVAILABLE_SEATS_KEY + classroomId + ":" + date;
        // Lua脚本：原子检查并扣减座位
        String luaScript =
                "local key = KEYS[1] " +
                        "local val = redis.call('GET', key) " +
                        "if not val then " + // Redis中不存在，从数据库加载
                        "   return -2 " + // -2表示需要初始化
                        "end " +
                        "if tonumber(val) > 0 then " +
                        "   return redis.call('DECR', key) " + // 扣减成功返回新值
                        "else " +
                        "   return -1 " + // -1表示座位不足
                        "end";
        DefaultRedisScript<Long> script = new DefaultRedisScript<>(luaScript, Long.class);
        Long result = stringRedisTemplate.execute(script, Collections.singletonList(key));

        if (result == -2) {
            // Redis键不存在，需初始化教室数据
            String lockKey = DAILY_CLASSROOM_AVAILABLE_SEATS_LOCK_KEY + classroomId + ":" + date;
            RLock lock = redissonClient.getLock(lockKey);
            lock.lock();
            try {
                // 双重判定，再执行一次redis lua原子扣减
                Long doubleResult = stringRedisTemplate.execute(script, Collections.singletonList(key));
                if (doubleResult != -2) {
                    // 说明已经被其他线程初始化过了，直接使用这个结果
                    result = doubleResult;
                } else {
                    // 从数据库中查询该教室信息
                    QueryWrapper<DailyClassroomAvailability> wrapper = new QueryWrapper<>();
                    wrapper.eq("classroom_id", classroomId)  // 数据库字段名
                            .eq("date", date);                // 数据库字段名
                    DailyClassroomAvailability dca = dailyClassroomAvailabilityMapper.selectOne(wrapper);

                    if (dca == null || dca.getAvailable() <= 0) {
                        // 防缓存穿透：设置短期占位值
                        stringRedisTemplate.opsForValue().set(key, "0", 30, TimeUnit.SECONDS);
                        return Result.error("无效的日期或者教室");
                    }
                    // 再采用lua脚本写入缓存并原子化扣减座位
                    String initAndDecrementScript =
                            "redis.call('SET', KEYS[1], ARGV[1], 'EX', " + DAILY_CLASSROOM_AVAILABLE_SEATS_TTL_STRING_SEC + ") " +
                                    "return redis.call('DECR', KEYS[1])";
                    DefaultRedisScript<Long> initScript = new DefaultRedisScript<>(initAndDecrementScript, Long.class);
                    result = stringRedisTemplate.execute(
                            initScript,
                            Collections.singletonList(key),
                            String.valueOf(dca.getAvailable())
                    );
                }
            } finally {
                lock.unlock();
            }
        }
        // 处理结果
        if (result == -1) {
            return Result.error("座位不足，扣减失败");
        } else if (result < 0) { // 处理其他负值（如-2未解决情况）
            return Result.error("系统异常，错误码：" + result);
        }
        return Result.success();
    }

    /**
     * 更新数据库
     */
    public void updateDatabase(Long userId, Long classroomId, LocalDate date) throws InterruptedException {
        // 1.创建预约记录并写入数据库
        Reservation reservation = new Reservation();
        reservation.setUserId(userId);
        reservation.setClassroomId(classroomId);
        reservation.setDate(date);
        reservation.setStatus(ReservationStatusEnum.ACTIVE_NOT_CHECKED);
        int inserted = reservationMapper.insert(reservation);

        if (inserted != 1) {
            // 如果插入失败，抛出异常以触发事务回滚
            throw new BusinessException("创建预约记录失败，请重试");
        }
        // 2.更新可用座位数
        UpdateWrapper<DailyClassroomAvailability> updateWrapper = new UpdateWrapper<>();
        updateWrapper.eq("classroom_id", classroomId)
                .eq("date", date)
                .gt("available", 0) // 重要：增加一个最终的数据库层面的校验，防止库存变成负数
                .setSql("available = available - 1");
        int updated = dailyClassroomAvailabilityMapper.update(updateWrapper);

        if (updated != 1) {
            throw new BusinessException("教室座位已满或数据异常，操作失败");
        }
    }

    // 处理失败回滚（如果数据库更新失败）
    public void rollbackRedis(Long classroomId, LocalDate date) {
        String key = DAILY_CLASSROOM_AVAILABLE_SEATS_KEY + classroomId + ":" + date;
        stringRedisTemplate.opsForValue().increment(key); // INCR回滚座位
    }

    // 发送消息通知，调用邮件服务
    public void sendReservationMQMessage(Long userId, Long classroomId, LocalDate reservationDate) {

        Map<String, Object> messagePayload = new HashMap<>();
        messagePayload.put("email", "2105135962@qq.com");
        messagePayload.put("classroomId", classroomId);
        messagePayload.put("reservationDate", reservationDate);

        // 统一的调用方式！
        // 对于RabbitMQ，"reservation.success" 是 routingKey
        // 对于RocketMQ，我们可以定义一个 topic 叫 "RESERVATION_TOPIC", tag是"success", 传参就是 "RESERVATION_TOPIC:success"
        String topic = "reservation.success"; // 简单起见，两边都用这个
        messageProducer.sendMessage(topic, messagePayload);

    }

    /**
     * 座位预约
     *
     * @param reservationDTO
     * @return
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Result reserveSeat(ReservationDTO reservationDTO) {
        Long classroomId = reservationDTO.getClassroomId();
        LocalDate date = reservationDTO.getDate();
        Long userId = reservationDTO.getUserId();
        // 1. 基础验证
        Result<String> validateRequestResult = validateRequest(classroomId, date, userId);
        if (validateRequestResult.getCode() != 1) {
            return validateRequestResult;
        }

        // 2. Redis扣减库存
        Result<String> decrementRedisResult = decrementRedisAvailability(classroomId, date);
        if (decrementRedisResult.getCode() != 1) {
            return decrementRedisResult;
        }

        // 3. 数据库操作（创建预约记录和扣减库存）
        try {
            // 在同一个事务中执行数据库操作
            updateDatabase(userId, classroomId, date);
        } catch (Exception e) {
            log.error("数据库操作失败，准备回滚Redis库存。教室ID: {}, 日期: {}", classroomId, date, e);
            // 数据库事务会因为异常自动回滚
            // 我们需要手动回滚Redis
            rollbackRedis(classroomId, date);
            // 再次抛出异常触发数据库数据回滚
            throw new BusinessException("预约失败，系统内部错误");
        }

        // 4.数据库事务成功后，发送消息通知调用邮件服务，
        // try {
        //     sendReservationMQMessage(userId, classroomId, date);
        // }catch(Exception e){
        //     log.error("发送MQ消息失败！错误: {}", e.getMessage());
        // }

        // 5.完成预约，返回成功结果
        return Result.success("预约成功");
    }

    /**
     * 获取单日单个教室可用座位数
     *
     * @param classroomId
     * @param date
     * @return
     */
    @Override
    public Result<Integer> getClassroomAvailableSeats(Long classroomId, LocalDate date) {
        // 先查redis
        String key = DAILY_CLASSROOM_AVAILABLE_SEATS_KEY + classroomId + ":" + date;
        String availableSeatsNumbersStr = stringRedisTemplate.opsForValue().get(key);
        Integer availableSeatsNumbers = -1;
        if (availableSeatsNumbersStr == null) {
            // Redis键不存在，需初始化教室数据
            String lockKey = DAILY_CLASSROOM_AVAILABLE_SEATS_LOCK_KEY + classroomId + ":" + date;
            RLock lock = redissonClient.getLock(lockKey);
            lock.lock();

            try {
                // 双重判定，再查询一次
                String doubleResult = stringRedisTemplate.opsForValue().get(key);
                if (doubleResult != null) {
                    // 说明已经被其他线程初始化过了，直接使用这个结果
                    availableSeatsNumbers = Integer.valueOf(doubleResult);
                } else {
                    // 从数据库中查询该教室信息
                    QueryWrapper<DailyClassroomAvailability> wrapper = new QueryWrapper<>();
                    wrapper.eq("classroom_id", classroomId)  // 数据库字段名
                            .eq("date", date);                // 数据库字段名
                    DailyClassroomAvailability dca = dailyClassroomAvailabilityMapper.selectOne(wrapper);
                    if (dca == null || dca.getAvailable() <= 0) {
                        // 防缓存穿透：设置短期占位值
                        stringRedisTemplate.opsForValue().set(key, "0", 30, TimeUnit.SECONDS);
                        return Result.error("无效的日期或者教室");
                    }
                    availableSeatsNumbers = dca.getAvailable();
                    stringRedisTemplate.opsForValue().set(key, dca.getAvailable().toString(),
                            DAILY_CLASSROOM_AVAILABLE_SEATS_TTL, DAILY_CLASSROOM_AVAILABLE_SEATS_TIMEUNIT);
                }
            } finally {
                lock.unlock();
            }
        } else {
            availableSeatsNumbers = Integer.valueOf(availableSeatsNumbersStr);
        }
        if (availableSeatsNumbers == -1) {
            return Result.error("查询失败");
        }
        return Result.success(availableSeatsNumbers);
    }


    /**
     * 取消预约
     *
     * @param reservationId
     * @return
     */
    @Override
    @Transactional(rollbackFor = Exception.class) // 保证数据库操作的原子性
    public Result cancelReservation(int reservationId) {
        // 1. 查询预约记录，并加悲观锁（FOR UPDATE），防止并发取消
        Reservation reservation = reservationMapper.selectById(reservationId); // 假设有这个方法

        if (reservation == null) {
            return Result.error("预约记录不存在");
        }
        if (reservation.getStatus() == ReservationStatusEnum.CANCELED) {
            return Result.error("预约已取消，请勿重复操作");
        }
        // 2. 更新预约记录状态
        reservation.setStatus(ReservationStatusEnum.CANCELED);
        reservationMapper.updateById(reservation);
        // 3. 回滚（增加）数据库中的座位数
        UpdateWrapper<DailyClassroomAvailability> updateWrapper = new UpdateWrapper<>();
        updateWrapper.eq("classroom_id", reservation.getClassroomId())
                .eq("date", reservation.getDate())
                .setSql("available = available + 1");
        dailyClassroomAvailabilityMapper.update(null, updateWrapper);
        // 4. 更新Redis（在数据库事务成功提交后执行）
        try {
            String key = DAILY_CLASSROOM_AVAILABLE_SEATS_KEY + reservation.getClassroomId() + ":" + reservation.getDate();
            // 直接 INCR 即可，因为这是对已有 key 的操作
            // 最好检查一下key是否存在，如果不存在，可能意味着缓存已过期，此时可以不操作或重新加载
            if (Boolean.TRUE.equals(stringRedisTemplate.hasKey(key))) {
                stringRedisTemplate.opsForValue().increment(key);
            }
        } catch (Exception e) {
            // 关键点2：处理缓存更新失败
            throw new BusinessException("取消预约成功，但回滚Redis座位数失败！需要手动补偿。ReservationId: {}");
            // 这里可以引入补偿机制，比如：
            // 1. 记录失败日志，通过定时任务扫描日志进行补偿。
            // 2. 将失败的消息发送到MQ，由消费者进行重试补偿。
            // 对于多数系统，记录详细日志供人工或脚本排查已是可接受的方案。
        }
        return Result.success();
    }

    /**
     * 签到
     * @param reservationId
     * @return
     */
    @Override
    public Result checkIn(Long reservationId) {
        // TODO 应对高并发的话应该也得用上redis吧,不过签到的话会高并发吗？
        // 检查该条预约信息
        Reservation reservation = reservationMapper.selectById(reservationId);
        if (reservation == null) {
            return Result.error("签到失败，不存在该条预约信息");
        }
        if(!reservation.getDate().isEqual(LocalDate.now())) {
            return Result.error("签到失败，签到日期错误");
        }
        if(reservation.getStatus()==ReservationStatusEnum.ACTIVE_CHECKED){
            return Result.error("您今天已经签到过，无需再次签到");
        }
        // 签到，修改数据库信息
        UpdateWrapper<Reservation> updateWrapper=new UpdateWrapper<>();
        updateWrapper.eq("id", reservation.getId()).set("status", ReservationStatusEnum.ACTIVE_CHECKED);
        reservationMapper.update(reservation, updateWrapper);
        return Result.success("签到成功");
    }
}
